/**
 * 
 */

"use strict";

var fields = fields || {};

fields.fields = [ [] ];
fields.by_id = {};
fields.el = null;
fields.COLUMNS = 0;
fields.ROWS = 0;
fields.ID = 0;

fields.init = function(groundElement, columns, rows) {
	// remove all elements
	if (fields.el)
		fields.el.children().remove();
	// initialize new ground
	fields.el = groundElement;
	fields.COLUMNS = columns;
	fields.ROWS = rows;
	fields.fields = new Array();
	fields.by_id = {};
	for (var c = 0; c < columns; ++c) {
		fields.fields[c] = new Array();
		for (var r = 0; r < rows; ++r) {
			fields.fields[c][r] = new Array();
			// make basic ground field
			var f = new fields.Ground(c, r);
			f.place();
		}
	}
};

fields.restore = function(backup_values) {

	var newField = undefined;
	if (backup_values && backup_values.cls && backup_values.cls[0] === 'field') {

		switch (backup_values.cls[1]) {
		case defs.FieldTypes.GROUND:
			newField = new fields.Ground(backup_values.column, backup_values.row);
			break;
		case defs.FieldTypes.RAIL:
			newField = new fields.Rail(backup_values.column, backup_values.row);
			break;
		case defs.FieldTypes.MOVABLE:
			switch (backup_values.cls[2]) {
			case 'train':
				newField = new movable.Train(backup_values.column, backup_values.row, backup_values.toDirection);
				break;
			default:
				newField = new movable.Movable(backup_values.column, backup_values.row, undefined, backup_values.toDirection);
				break;
			}
			break;
		case defs.FieldTypes.SIGNAL:
			newField = new signals.Signal(backup_values.column, backup_values.row, defs.Direction.UP);
			signals.signals.push(newField);
			break;
		case defs.FieldTypes.SWITCH:
			newField = new signals.Switch(backup_values.column, backup_values.row, defs.Direction.UP);
			signals.signals.push(newField);
			break;
		default:
			newField = new fields.Field(backup_values.column, backup_values.row);
			break;
		}

	} else {
		console.err('traintest.restore() --> unknown field base type found ('
			+ backup_values.cls + ') --> ignored');
		return undefined;
	}

	// apply field specific things
	if (newField instanceof fields.Field) {
		newField.id = backup_values.id;
		if (newField.id) {
			if (newField.id >= fields.ID)
				fields.ID = newField.id + 1;
			$(newField.el).data('field_id', newField.id);
		}
		newField.vec2.x = backup_values.x;
		newField.vec2.y = backup_values.y;
		if (backup_values.a !== undefined)
			newField.vec2 = newField.vec2.addA(backup_values.a);
		fields.Field.prototype.setInnerPos.call(newField, newField.vec2);
		// restore classes
		newField.cls = [];
		backup_values.cls.foreach(function(i, value, first, last) {
			newField.el.addClass(value);
			newField.cls.push(value);
		});
		// remove applied values
		backup_values.id = undefined;
		backup_values.x = undefined;
		backup_values.y = undefined;
		backup_values.a = undefined;
		backup_values.cls = undefined;
		backup_values.column = undefined;
		backup_values.row = undefined;
	}

	// apply movable specific things
	if (newField instanceof movable.Movable) {
		newField.fromDirection = backup_values.fromDirection;
		newField.toDirection = backup_values.toDirection;
		newField.applyDistance(backup_values.distance);
		backup_values.fromDirection = undefined;
		backup_values.toDirection = undefined;
		backup_values.distance = undefined;
	}

	// apply remaining values generically
	for ( var val in backup_values) {
		if (backup_values[val] !== undefined && val in newField) {
			newField[val] = backup_values[val];
			backup_values[val] = undefined;
		}
	}
	fields.by_id[newField.id] = newField;
	return newField;
};

fields.Position = function(left, top) {
	this.left = left || 0;
	this.top = top || 0;
};
fields.Position.prototype = {
	left : 0,
	top : 0
};
fields.Position.prototype.equals = function(other) {
	return this.left.fuzzyEquals(other.left) && this.top.fuzzyEquals(other.top);
};

fields.FieldPosition = function(col, row) {
	fields.FieldPosition.prototype.setPos.call(this, col, row);
};
fields.FieldPosition.prototype = {
	row : 0,
	column : 0
};
fields.FieldPosition.prototype.getFields = function() {
	return fields.fields[this.column][this.row];
};
fields.FieldPosition.prototype.getFieldsAtZ = function(z_index) {
	var result = [];
	var elems = fields.FieldPosition.prototype.getFields.call(this);
	elems.foreach(function(i, value, first, last) {
		var z = value.el.css('z-index');
		if (z !== undefined && z == z_index)
			result.push(value);
	});
	return result;
};
fields.FieldPosition.prototype.setPos = function(col, row) {
	var c = (col || 0) % fields.COLUMNS;
	var r = (row || 0) % fields.ROWS;
	this.column = c < 0 ? fields.COLUMNS + c : c;
	this.row = r < 0 ? fields.ROWS + r : r;
};
fields.FieldPosition.prototype.setPosCss = function(left, top) {
	var newLeft, newTop, c, r;
	newLeft = left - left % defs.FIELD_WIDTH;
	c = newLeft / defs.FIELD_WIDTH;
	newTop = top - top % defs.FIELD_HEIGHT;
	r = newTop / defs.FIELD_HEIGHT;
	this.setPos(c, r);
};
fields.FieldPosition.prototype.getNext = function(direction) {
	if (!direction instanceof Number)
		return null;
	var nxt = null;
	var dir = direction % 5;
	switch (dir) {
	case defs.Direction.UP:
		nxt = new fields.FieldPosition(this.column, this.row - 1);
		break;
	case defs.Direction.RIGHT:
		nxt = new fields.FieldPosition(this.column + 1, this.row);
		break;
	case defs.Direction.DOWN:
		nxt = new fields.FieldPosition(this.column, this.row + 1);
		break;
	case defs.Direction.LEFT:
		nxt = new fields.FieldPosition(this.column - 1, this.row);
		break;
	default:
		return null;
	}
	return nxt;
};
fields.FieldPosition.prototype.getLeftTop = function() {
	return new fields.Position(this.column * defs.FIELD_WIDTH, this.row
		* defs.FIELD_HEIGHT);
};
fields.FieldPosition.prototype.getCenterPos = function() {
	return new fields.Position(Math.floor((this.column + 0.5)
		* defs.FIELD_WIDTH), Math.floor((this.row + 0.5) * defs.FIELD_HEIGHT));
};
fields.FieldPosition.prototype.place = function() {
	fields.fields[this.column][this.row].push(this);
};
fields.FieldPosition.prototype.remove = function() {
	fields.fields[this.column][this.row].remove(this);
};
fields.FieldPosition.prototype.placed = function() {
	return fields.fields[this.column][this.row].indexOf(this) >= 0;
};
fields.FieldPosition.prototype.equals = function(other) {
	return this.column === other.column && this.row === other.row;
};

fields.Action = function(field_id, fn_name, args, html) {
	this.field_id = field_id;
	this.fn_name = fn_name;
	this.args = args;
	this.html = html;
};

fields.Field = function(col, row, cls) {
	this.id = undefined;
	if (col == undefined || row == undefined)
		return;
	fields.FieldPosition.call(this, col, row);
	this.el = $('<div class="field"></div>');
	this.cls = this.cls || new Array();
	this.cls.push('field');
	this.vec2 = new utils.Vec2A(0, 0, 0);
	this.actions = [];
	if (typeof cls === 'string') {
		this.cls.push(cls);
		this.el.addClass(cls);
	}
	// add possible moving directions
	this.directions = [ [ defs.Direction.UP, defs.Direction.DOWN ],
		[ defs.Direction.RIGHT, defs.Direction.LEFT ],
		[ defs.Direction.UP, defs.Direction.RIGHT ],
		[ defs.Direction.UP, defs.Direction.LEFT ],
		[ defs.Direction.DOWN, defs.Direction.RIGHT ],
		[ defs.Direction.DOWN, defs.Direction.LEFT ] ];
	// set column and row position
	this.setPos(col, row);
	// add rotation possibility
	var thisField = this;
	$(this.el).on('click', function(e) {
		if (!e.ctrlKey)
			return;
		if (thisField.placed())
			return;
		switch (e.which) {
		case 1: // left mouse click
			if (thisField.rotate)
				thisField.rotate();
			break;
		default:
			return;
		}
		// prevent execute other event handler
		e.stopPropagation();
	});
	// register the action receiver
	fields.el.on('action_receive', function(e, action) {
		if (action.field_id === thisField.id
			&& typeof thisField[action.fn_name] === 'function')
			// execute action in this context
			thisField[action.fn_name].apply(thisField, action.args);
	});
};
fields.Field.prototype = new fields.FieldPosition();
fields.Field.prototype.backup = function() {
	if (this.id !== undefined && typeof this.id === 'number')
		return {
			id : this.id,
			column : this.column,
			row : this.row,
			x : this.vec2.x,
			y : this.vec2.y,
			a : this.vec2.a,
			cls : this.cls,
			speed : this.speed,
			distance : this.distance,
			fromDirection : this.fromDirection,
			toDirection : this.toDirection,
			directions : this.directions,
			direction : this.direction,
			track_field_id : this.track_field_id,
			activeDirSet : this.activeDirSet,
			actions : this.actions,
			events : this.events,
			mov_fields_info : this.mov_fields_info
		};
	else
		return null;
};
fields.Field.prototype.remove = function() {
	if (this.placed()) {
		fields.FieldPosition.prototype.remove.call(this);
		fields.by_id[this.id] = undefined;
	}
	this.el.remove();
};
fields.Field.prototype.getSiblings = function(field, dept) {
	var matchType = this;
	var result = [];
	if (field instanceof fields.Field)
		matchType = field;
	var className;
	if (typeof dept === 'number')
		className = matchType.cls
			.slice(0, Math.min(matchType.cls.length, dept)).join(".");
	else
		className = matchType.cls.join(".");
	var f = this.getFields();
	if (f)
		f.foreach(function(i, value, first, last) {
			if (value instanceof fields.Field
				&& value.cls.join(".").indexOf(className, 0) === 0)
				result.push(value);
		});
	return result;
};
fields.Field.prototype.canPlace = function() {
	var result = false;
	if (this.getSiblings().length > 0) {
		console.log('fields.Field.prototype.place() --> type of element '
			+ this.cls.join(".") + ' already placed');
	} else {
		var placed = this.placed();
		if (!placed)
			fields.el.append(this.el);
		if (this.getFieldsAtZ(this.el.css('z-index')).length > 0) {
			console
				.log('fields.Field.prototype.place() --> element(s) already placed on the same z-index '
					+ this.el.css('z-index')
					+ ' (try to place '
					+ this.cls.join('.') + ')');
		} else
			result = true;
		if (!placed)
			this.el.remove();
	}
	return result;
};
fields.Field.prototype.place = function(no_id) {
	if (!this.placed() && this.canPlace()) {
		fields.FieldPosition.prototype.place.call(this);
		fields.el.append(this.el);
		if (no_id)
			this.id = undefined;
		else {
			this.id = fields.ID++;
			fields.by_id[this.id] = this;
			$(this.el).data('field_id', this.id);
			this.getActions();
		}
		return true;
	} else
		return false;
};
fields.Field.prototype.getLeftTop = function() {
	var absPos = fields.FieldPosition.prototype.getLeftTop.call(this);
	absPos.left += ((this.vec2.x * defs.FIELD_WIDTH).round());
	absPos.top += ((this.vec2.y * defs.FIELD_HEIGHT).round());
	return absPos;
};
fields.Field.prototype.getCenterPos = function(opt_vec2_offset) {
	var xOffs = opt_vec2_offset.x || 0;
	var yOffs = opt_vec2_offset.y || 0;
	var relPos = new fields.Position(defs.FIELD_WIDTH * (this.vec2.x + xOffs), defs.FIELD_HEIGHT
		* (this.vec2.y + yOffs));
	var absPos = fields.FieldPosition.prototype.getCenterPos.call(this);
	return new fields.Position(absPos.left + relPos.left, absPos.top
		+ relPos.top);
};
fields.Field.prototype.setPos = function(col, row) {
	var wasPlaced;
	if (wasPlaced = this.placed())
		this.remove();
	fields.FieldPosition.prototype.setPos.call(this, col, row);
	var vec2 = new utils.Vec2(0, 0);
	if (this.vec2.a !== undefined)
		vec2 = vec2.addA(this.vec2.a);
	this.setInnerPos(vec2);
	if (wasPlaced)
		this.place();
};
fields.Field.prototype.setInnerPos = function(vec2) {
	var absPos, angle = 0;
	if (vec2 instanceof utils.Vec2A)
		angle = vec2.a;
	else
		vec2 = vec2.addA(angle);
	this.vec2 = vec2;
	absPos = this.getLeftTop();
	this.el.css({
		left : absPos.left + 'px',
		top : absPos.top + 'px',
		transform : 'rotate(' + angle + 'deg)'
	});
};
fields.Field.prototype.rotate = function(angle) {
	if (typeof angle !== 'number')
		angle = 90;
	var vec2a = this.vec2.addA(angle % 360);
	this.setInnerPos(vec2a);
};
fields.Field.prototype.getPlacePosAngle = function(dir_from, dir_to, distance) {
	var angle = 0, dir = dir_to;
	var dist = Math.max(0, Math.min(1, Math.abs(distance || (1 / 2))));
	// calculate angle
	if (dist > 0.5) {
		if (dir_from === dir_to) {
			angle = 180;
		} else if (defs.CounterDirection[dir_from] !== dir_to)
			if ((dir_from === defs.Direction.UP && dir_to === defs.Direction.RIGHT)
				|| (dir_from === defs.Direction.UP && dir_to === defs.Direction.RIGHT)
				|| (dir_from === defs.Direction.RIGHT && dir_to === defs.Direction.DOWN)
				|| (dir_from === defs.Direction.DOWN && dir_to === defs.Direction.LEFT)
				|| (dir_from === defs.Direction.LEFT && dir_to === defs.Direction.UP)) {
				angle = -90;
			} else {
				angle = 90;
			}
	} else {
		dir = defs.CounterDirection[dir_from];
	}
	switch (dir) {
	case defs.Direction.UP:
		return new utils.Vec2A(0, dist - 0.5, angle);
	case defs.Direction.DOWN:
		return new utils.Vec2A(0, 0.5 - dist, angle);
	case defs.Direction.RIGHT:
		return new utils.Vec2A(dist - 0.5, 0, angle);
	case defs.Direction.LEFT:
		return new utils.Vec2A(0.5 - dist, 0, angle);
	}
};
fields.Field.prototype.getNext = function(direction) {
	var next = fields.FieldPosition.prototype.getNext.call(this, direction);
	var sibl;
	if (next && (sibl = this.getSiblings.call(next, this)).length > 0)
		return sibl[0];
	return null;
};
fields.Field.prototype.getActions = function() {
	return this.actions;
};

fields.Ground = function(col, row, cls) {
	if (col === undefined || row === undefined)
		return;
	fields.Field.call(this, col, row, 'ground');
	if (typeof cls === 'string') {
		this.el.addClass(cls);
		this.cls.push(cls);
	}
};
fields.Ground.prototype = new fields.Field();
fields.Ground.prototype.place = function() {
	fields.Field.prototype.place.call(this, (this.cls.last() === 'ground'));
};

fields.Rail = function(col, row, cls) {
	if (col === undefined || row === undefined)
		return;
	fields.Field.call(this, col, row, 'rail');
	this.activeDirSet = -1;
	if (typeof cls === 'string') {
		this.el.addClass(cls);
		this.cls.push(cls);
		this.directions = defs.RailTypes[cls];
	} else {
		this.directions = null;
	}
};
fields.Rail.prototype = new fields.Ground();
fields.Rail.prototype.switchDir = function(dir_set_idx) {
	if (dir_set_idx === null || dir_set_idx < 0 || !this.directions
		|| this.directions.length <= 1) {
		this.activeDirSet = -1;
		return;
	}
	var dir_idx = (dir_set_idx || (this.activeDirSet + 1))
		% this.directions.length;
	this.activeDirSet = dir_idx;
};
fields.Rail.prototype.getDirSetIdx = function(dir_from, dir_to) {
	if (!this.directions)
		return -1;
	var result = this.directions.foreach(function(i, value, first, last) {
		if ((dir_from === undefined || value.indexOf(dir_from) >= 0)
			&& (dir_to === undefined || value.indexOf(dir_to) >= 0))
			return i;
		if (last)
			return -1;
	});
	return result;
};
fields.Rail.prototype.canMove = function(dir_from, dir_to) {
	if (!this.directions)
		return false;
	var dirSet = this.getDirSetIdx(dir_from, dir_to);
	if (dirSet >= 0 && (this.activeDirSet === dirSet || this.activeDirSet < 0))
		return true;
	return false;
};
fields.Rail.prototype.getDistance = function(dir_from, dir_to) {
	if (dir_from === undefined || dir_to === undefined)
		return 0;
	else if (defs.CounterDirection[dir_from] === dir_to || dir_from === dir_to)
		/* linear */
		return 1;
	else
		/* circle */
		return Math.PI / 4;
};
fields.Rail.prototype.rotate = undefined; // disable rotation
fields.Rail.prototype.getPlacePosAngle = function(dir_from, dir_to, distance) {
	if (this.getDirSetIdx(dir_from, dir_to) < 0)
		throw new Error('fields.Rail.prototype.getPlacePosAngle() --> requested direction combination not supported, from: '
			+ dir_from + ' to: ' + dir_to);
	var x = 0, y = 0, quad = null;
	var quadrants = {
		lt : [ 90, 180 ],
		rt : [ 180, 270 ],
		rb : [ 270, 360 ],
		lb : [ 0, 90 ]
	};
	var maxDist = this.getDistance(dir_from, dir_to);
	distance = distance || (maxDist / 2);
	var dist = Math.max(0, Math.min(maxDist, Math.abs(distance)));

	if (defs.CounterDirection[dir_from] === dir_to || dir_from === dir_to) {
		/* linear position */
		if (dir_from === defs.Direction.RIGHT)
			return new utils.Vec2A(0.5 - dist, 0, 0);
		else if (dir_from === defs.Direction.LEFT)
			return new utils.Vec2A(-0.5 + dist, 0, 0);
		else if (dir_from === defs.Direction.UP)
			return new utils.Vec2A(0, -0.5 + dist, 0);
		else
			return new utils.Vec2A(0, 0.5 - dist, 0);
	}

	/* calculate radial position */
	var angle = (dist * 360) / (Math.PI || 3.14);
	var a = 0;

	/* calculate angle by start point */
	switch (dir_from) {
	case 1 /* UP */:
		if (dir_to === defs.Direction.LEFT) {
			a = angle;
			quad = quadrants.lt;
			angle += 90;
		} else if (dir_to === defs.Direction.RIGHT) {
			a = -angle;
			quad = quadrants.rt;
			angle = 270 - angle;
		} else
			console
				.log('fields.Rail.prototype.getPlacePosAngle() --> unknown moving profile (from: '
					+ dir_from + ' to: ' + dir_to + ')');
		break;
	case 2 /* RIGHT */:
		if (dir_to === defs.Direction.UP) {
			a = angle;
			quad = quadrants.rt;
			angle += 180;
		} else if (dir_to === defs.Direction.DOWN) {
			a = -angle;
			quad = quadrants.rb;
			angle = 360 - angle;
		} else
			console
				.log('fields.Rail.prototype.getPlacePosAngle() --> unknown moving profile (from: '
					+ dir_from + ' to: ' + dir_to + ')');
		break;
	case 3 /* DOWN */:
		if (dir_to === defs.Direction.LEFT) {
			a = -angle;
			quad = quadrants.lb;
			angle = 90 - angle;
		} else if (dir_to === defs.Direction.RIGHT) {
			a = angle;
			quad = quadrants.rb;
			angle += 270;
		} else
			console
				.log('fields.Rail.prototype.getPlacePosAngle() --> unknown moving profile (from: '
					+ dir_from + ' to: ' + dir_to + ')');
		break;
	case 4 /* LEFT */:
		if (dir_to === defs.Direction.UP) {
			a = -angle;
			quad = quadrants.lt;
			angle = 180 - angle;
		} else if (dir_to === defs.Direction.DOWN) {
			a = angle;
			quad = quadrants.lb;
			;
		} else
			console
				.log('fields.Rail.prototype.getPlacePosAngle() --> unknown moving profile (from: '
					+ dir_from + ' to: ' + dir_to + ')');
		break;
	default:
		throw new Error('fields.Rail.prototype.getPlacePosAngle() --> direction from ('
			+ dir_from + ') is unsupported');
	}
	// normalize angle
	angle = angle % 360;
	// get radiant (pieces of 2PI)
	var rad = angle * 2 * (Math.PI || 3.14) / 360;
	// calculate absolute SIN and COS values and scale to coordinate system
	var xOffs = Math.abs(Math.sin(rad) / 2);
	var yOffs = Math.abs(Math.cos(rad) / 2);
	// calculate relative positions
	if (quad == quadrants.lt) {
		x = -0.5 + xOffs;
		y = -0.5 + yOffs;
	} else if (quad == quadrants.rt) {
		x = 0.5 - xOffs;
		y = -0.5 + yOffs;
	} else if (quad == quadrants.rb) {
		x = 0.5 - xOffs;
		y = 0.5 - yOffs;
	} else if (quad == quadrants.lb) {
		x = -0.5 + xOffs;
		y = 0.5 - yOffs;
	}
	/* calculate position vector */
	return new utils.Vec2A(x, y, a);
};
fields.Rail.prototype.getToDirs = function(from_direction) {
	var result = [];
	this.directions.foreach(function(i, value, first, last) {
		var idxFrom = value.indexOf(from_direction);
		var idxTo = (idxFrom + 1) % 2;
		if (idxFrom >= 0 && value[idxTo] !== from_direction) {
			result.push(value[idxTo]);
		}
	});
	return result;
};
fields.Rail.prototype.getNext = function(to_direction) {
	var next = fields.FieldPosition.prototype.getNext.call(this, to_direction);
	var sibl;
	if (next && (sibl = this.getSiblings.call(next, this, 2)).length > 0)
		return sibl[0];
	return null;
};
fields.Rail.prototype.getActions = function() {
	var actions = [];
	var thisId = this.id;
	// action switch active direction
	if (this.directions.length > 1) {
		// toggle switch direction
		actions
			.push(new fields.Action(thisId, 'switchDir', [], '<div class="action_entry"><div class="action switch">TOGGLE</div></div>'));
		// append all possible direction sets
		this.directions
			.foreach(function(i, value, first, last) {
				// make a closure call, to be sure that 'i' is a copy instead a
				// reference
				actions
					.push((function(id) {
						return new fields.Action(id, 'switchDir', [ i ], '<div class="action_entry"><div class="action switch">SWITCH</div>'
							+ '<div class="action dir '
							+ defs.Direction.toString(value[0])
							+ '">'
							+ defs.Direction.toString(value[0])
							+ '</div>'
							+ '<div class="action dir '
							+ defs.Direction.toString(value[1])
							+ '">'
							+ defs.Direction.toString(value[1])
							+ '</div></div>');
					})(thisId));
			});
	}
	this.actions = actions;
	return actions;
};
